package org.exobject;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;

/**
 * The exObject class is an object that can wrap any type of java object, or primitive, including
 * null types and void types. There are several constructors each taking one of the primitives
 * or objects, and two static methods for creating object of null or void types. Each
 * constructor has a corresponding getValue and setValue functions that each take
 * or return a value of the correct type. There is also a corresponding isType function
 * to programmatically check the type of the object, or alternately, getClassification,
 * which returns an enum of the type. If a get or set function is called inappropriately,
 * a WrongClassTypeException is thrown. Note that this class has special support
 * for Strings, because they are so special. Technically they are Objects, but this
 * allows for more specifically describing the type.
 * @author Layton Smith
 */
public class exObject extends Object implements Serializable, Cloneable{

    private static final long serialVersionUID = -8474343029729107769L;

    //Each constructor (for the most part) sets these three values: type, classification,
    //and the corresponding Val.
    private Class type;
    private Classification classification;

    private Object objectVal;
    private String stringVal;
    private byte byteVal;
    private short shortVal;
    private int intVal;
    private long longVal;
    private float floatVal;
    private double doubleVal;
    private boolean booleanVal;
    private char charVal;

    private transient ArrayList<exObjectChangeListener> listeners = null;


    /*/ Clonable Interface *****************************************************/

    @Override
    public exObject clone() throws CloneNotSupportedException {
        exObject e = new exObject();
        e.classification = classification;
        e.type = type;
        if(classification == Classification.NULL){
            return e;
        } else if(classification == Classification.VOID){
            return e;
        } else if(classification == Classification.OBJECT){
            Method[] m = objectVal.getClass().getDeclaredMethods();
            Method clone = null;
            for(int i = 0; i < m.length; i++){
                Method n = m[i];
                if(n.getName().equals("clone")){
                    clone = n;
                    break;
                }
            }
            if(clone == null){
                throw new CloneNotSupportedException();
            } else{
                try {
                    e.objectVal = clone.invoke(objectVal, new Object[]{});
                    return e;
                } catch (Exception ex) {
                    throw new CloneNotSupportedException();
                }
            }
        } else if(classification == Classification.STRING){
            e.stringVal = stringVal;
            return e;
        } else if(classification == Classification.BOOLEAN){
            e.booleanVal = booleanVal;
            return e;
        } else if(classification == Classification.BYTE){
            e.byteVal = byteVal;
            return e;
        } else if(classification == Classification.CHAR){
            e.charVal = charVal;
            return e;
        } else if(classification == Classification.DOUBLE){
            e.doubleVal = doubleVal;
            return e;
        } else if(classification == Classification.FLOAT){
            e.floatVal = floatVal;
            return e;
        } else if(classification == Classification.INT){
            e.intVal = intVal;
            return e;
        } else if(classification == Classification.LONG){
            e.longVal = longVal;
            return e;
        } else if(classification == Classification.SHORT){
            e.shortVal = shortVal;
            return e;
        } else {
            throw new CloneNotSupportedException();
        }
    }






    /*/ General functions ******************************************************/
    /**
     * Private, no argument constructor for use with static functions
     */
    private exObject(){}

    /**
     * This constructor allows for a generic creation of a primitive object. It
     * allows for a primitive to be passed as a string, and the desired type
     * to be passed as a string, and the value will automatically be parsed
     * into the appropriate value. Valid values for type are any of the 8 primitive
     * types + the string type. Neither type nor value may be null, or a WrongClassTypeException is
     * thrown. Boolean types are handled slightly differently from Boolean.parseBoolean, as numeric
     * values are valid. 0 is false, and any other value is true.
     *
     * <p>In general, this function is used for dynamically parsing a primitive from a string,
     * if the type of the value is also stored as a string.</p>
     * @param type
     * @param value
     */
    public static exObject getPrimitive(String type, String value){
        if(type == null || value == null){
            throw new WrongClassTypeException("");
        }
        if(type.equals("long")){
            return new exObject(Long.parseLong(value));
        } else if(type.equals("float")){
            return new exObject(Float.parseFloat(value));
        } else if(type.equals("double")){
            return new exObject(Double.parseDouble(value));
        } else if(type.equals("boolean")){
            //try to parse it as a number first
            try{
                int i = Integer.parseInt(value);
                //if it's 0, return false
                if(i == 0){
                    return new exObject(false);
                } else {
                //else return true
                    return new exObject(true);
                }
            } catch(NumberFormatException e){}
            //it's not a number, parse it regularly
            return new exObject(Boolean.parseBoolean(value));
        } else if(type.equals("char")){
            if(value.length() != 0){
                throw new WrongClassTypeException("Value has more than 1 character, even though char type was specified");
            } else{
                return new exObject(value.charAt(0));
            }
        } else if(type.equals("int")){
            return new exObject(Integer.parseInt(value));
        } else if(type.equals("byte")){
            return new exObject(Byte.parseByte(value), byte.class);
        } else if(type.equals("short")){
            return new exObject(Short.parseShort(value));
        } else if(type.toLowerCase().equals("string")){
            return new exObject(value);
        } else{
            throw new WrongClassTypeException("Type '" + type + "' isn't supported in the getPrimitive function");
        }
    }

    /**
     * This is a convenience function to send a Classification enum instead of a String.
     * @param type The type of object expected
     * @param value The value of the object
     * @return a new exObject
     * @see
     */
    public static exObject getPrimitive(Classification type, String value){
        return getPrimitive(type.toString().toLowerCase(), value);
    }

    /**
     * <p>This function allows you to return the value of the wrapped value as
     * the desired type, if it at all makes sense. Some combinations won't map,
     * however, such as returning most objects as a primitive, but most primitives
     * can be mapped to each other (including boxed primitives), and this
     * function allows that "cast" to occur. The exception to this rule is that
     * if a value is being mapped to a type that has a lower range, if the values
     * would not be equivalent after the cast, (that is, casting to another primitive,
     * then casting back, should be equivalent to the original value) then an
     * ArithmeticException is thrown. The precision of this operation depends on
     * the precision of BigDecimal and BigInteger though, and is not 100% precise
     * when converting between floating point and integer values.
     * If you don't care about the cast being particularly
     * accurate, return the type as long or double, then do the cast yourself.
     * Unlike in Java itself, booleans can be mapped to any other type, and vice versa.
     * In general, 0 is false, and any other number
     * is true (including negatives). When casting from a boolean to another type,
     * 1 (or the type equivalent) is returned for true.</p>
     * <p>
     * Consider the following code:
     * <pre>
     * exObject first = new exObject(2);
     * exObject second =
     * first.getAsType(Classification.BOOLEAN).getAsType(Classification.INTEGER);
     * int i = second.getInt();
     * </pre>
     * Given the way ranges for other type are considered, one might assume
     * that i is 2. This is not the case however. This is the only exception
     * to the range rule defined above.
     *
     * Also
     *
     * </p>
     * @param type The desired type
     * @return a new exObject "cast" as {@code type}, unless a WrongClassType exception
     * is thrown, or an ArithmeticException.
     */
    public exObject getAsType(Classification type) throws WrongClassTypeException, ArithmeticException{
        if(classification == Classification.OBJECT || this.toObject() == null){
            throw new WrongClassTypeException("Cannot \"cast\" the value of this object to a primitive");
        }
        if(classification == Classification.BOOLEAN){
            if(type == Classification.BOOLEAN){
                if(booleanVal == true){
                    return new exObject(true);
                } else{
                    return new exObject(false);
                }
            }
            if(type == Classification.BYTE){
                byte b;
                if(booleanVal == true){
                    b = 1;
                } else{
                    b = 0;
                }
                return new exObject(b, null);
            }
            if(type == Classification.CHAR){
                if(booleanVal == true){
                    return new exObject('\u0001');
                } else{
                    return new exObject('\u0000');
                }
            }
            if(type == Classification.DOUBLE){
                if(booleanVal == true){
                    return new exObject(1.0d);
                } else{
                    return new exObject(0.0d);
                }
            }
            if(type == Classification.FLOAT){
                if(booleanVal == true){
                    return new exObject(1.0f);
                } else{
                    return new exObject(0.0f);
                }
            }
            if(type == Classification.INT){
                if(booleanVal == true){
                    return new exObject(1);
                } else{
                    return new exObject(0);
                }
            }
            if(type == Classification.LONG){
                if(booleanVal == true){
                    return new exObject(1L);
                } else{
                    return new exObject(0L);
                }
            }
            if(type == Classification.SHORT){
                short s;
                if(booleanVal == true){
                    s = 1;
                } else{
                    s = 0;
                }
                return new exObject(s);
            }
        }
        if(classification == Classification.BYTE){
            byte b = byteVal;
            if(type == Classification.BOOLEAN){
                if(b == 0){
                    return new exObject(false);
                } else{
                    return new exObject(true);
                }
            }
            if(type == Classification.BYTE){
                return new exObject(b, null);
            }
            if(type == Classification.CHAR){
                return new exObject((char)b);
            }
            if(type == Classification.DOUBLE){
                return new exObject((double)b);
            }
            if(type == Classification.FLOAT){
                return new exObject((float)b);
            }
            if(type == Classification.INT){
                return new exObject((int)b);
            }
            if(type == Classification.LONG){
                return new exObject((long)b);
            }
            if(type == Classification.SHORT){
                return new exObject((short)b);
            }
        }
        if(classification == Classification.CHAR){
            char c = charVal;
            if(type == Classification.BOOLEAN){
                if(c == '\u0000'){
                    return new exObject(false);
                } else{
                    return new exObject(true);
                }
            }
            if(type == Classification.BYTE){
                //as long as the char is under 255 (0x00FF) there is no loss of
                //precision, so we can create a meaningful object, otherwise we
                //throw an exception
                if(c < 0xFF){
                    return new exObject((byte)c);
                } else{
                    throw new ArithmeticException("The wrapped char is over 0xFF, "
                            + "so we can't return a meaningful value");
                }
            }
            if(type == Classification.CHAR){
                return new exObject(c);
            }
            if(type == Classification.DOUBLE){
                return new exObject((double)c);
            }
            if(type == Classification.FLOAT){
                return new exObject((float)c);
            }
            if(type == Classification.INT){
                return new exObject((int)c);
            }
            if(type == Classification.LONG){
                return new exObject((long)c);
            }
            if(type == Classification.SHORT){
                return new exObject((short)c);
            }
        }
        if(classification == Classification.DOUBLE){
            double d = doubleVal;
            if(type == Classification.BOOLEAN){
                if(d == 0){
                    return new exObject(false);
                } else{
                    return new exObject(true);
                }
            }
            if(type == Classification.BYTE){
                //we piggy back off of BigDecimal to convert to a byte.
                //Note that This could throw an ArithmenticException
                return new exObject(BigDecimal.valueOf(d).byteValueExact());
            }
            if(type == Classification.CHAR){
                long l = BigDecimal.valueOf(d).toBigIntegerExact().longValue();
                if(l < 0xFFFF){
                    return new exObject((char)l);
                } else{
                    throw new ArithmeticException("The wrapped double can not be properly converted"
                            + " to a char");
                }
            }
            if(type == Classification.DOUBLE){
                return new exObject(d);
            }
            if(type == Classification.FLOAT){
                long l = BigDecimal.valueOf(d).toBigIntegerExact().longValue();
                if(l < 0xFFFFFFFF){
                    return new exObject((float)l);
                } else{
                    throw new ArithmeticException("The wrapped double can not be properly converted"
                            + " to a float");
                }
            }
            if(type == Classification.INT){
                long l = BigDecimal.valueOf(d).toBigIntegerExact().longValue();
                if(l < 0xFFFFFFFF){
                    return new exObject((int)l);
                } else{
                    throw new ArithmeticException("The wrapped double can not be properly converted"
                            + " to an int");
                }
            }
            if(type == Classification.LONG){
                return new exObject(BigDecimal.valueOf(d).toBigIntegerExact().longValue());
            }
            if(type == Classification.SHORT){
                long l = BigDecimal.valueOf(d).toBigIntegerExact().longValue();
                if(l < 0xFFFF){
                    return new exObject((short)l);
                } else{
                    throw new ArithmeticException("The wrapped double can not be properly converted"
                            + " to a short");
                }
            }
        }
        if(classification == Classification.FLOAT){
            float f = floatVal;
            if(type == Classification.BOOLEAN){
                if(f == 0){
                    return new exObject(false);
                } else{
                    return new exObject(true);
                }
            }
            if(type == Classification.BYTE){
                return new exObject(BigDecimal.valueOf(f).byteValueExact());
            }
            if(type == Classification.CHAR){
                long l = BigDecimal.valueOf(f).toBigIntegerExact().longValue();
                if(l < 0xFFFF){
                    return new exObject((char)l);
                } else{
                    throw new ArithmeticException("The wrapped float can not be properly converted "
                            + "to a char");
                }
            }
            if(type == Classification.DOUBLE){
                return new exObject((double)f);
            }
            if(type == Classification.FLOAT){
                return new exObject(f);
            }
            if(type == Classification.INT){
                long l = BigDecimal.valueOf(f).toBigIntegerExact().longValue();
                if(l < 0xFFFFFFFF){
                    return new exObject((int)l);
                } else{
                    throw new ArithmeticException("The wrapped float can not be properly converted"
                            + " to a int");
                }
            }
            if(type == Classification.LONG){
                return new exObject(BigDecimal.valueOf(f).toBigIntegerExact().longValue());
            }
            if(type == Classification.SHORT){
                long l = BigDecimal.valueOf(f).toBigIntegerExact().longValue();
                if(l < 0xFFFF){
                    return new exObject((short)l);
                } else{
                    throw new ArithmeticException("The wrapped float can not be properly converted"
                            + " to a short");
                }
            }
        }
        if(classification == Classification.INT){
            int i = intVal;
            if(type == Classification.BOOLEAN){
                if(i == 0){
                    return new exObject(false);
                } else{
                    return new exObject(true);
                }
            }
            if(type == Classification.BYTE){
                if(i < 0xFF){
                    return new exObject((byte)i);
                } else{
                    throw new ArithmeticException("The wrapped int can not be properly converted"
                            + " to a byte");
                }
            }
            if(type == Classification.CHAR){
                if(i < 0xFFFF){
                    return new exObject((char)i);
                } else {
                    throw new ArithmeticException("The wrapped int can not be properly converted"
                            + " to a char");
                }
            }
            if(type == Classification.DOUBLE){
                return new exObject(BigInteger.valueOf(i).doubleValue());
            }
            if(type == Classification.FLOAT){
                return new exObject((float)BigInteger.valueOf(i).doubleValue());
            }
            if(type == Classification.INT){
                return new exObject(i);
            }
            if(type == Classification.LONG){
                return new exObject((long)i);
            }
            if(type == Classification.SHORT){
                if(i < 0xFFFF){
                    return new exObject((short)i);
                } else{
                    throw new ArithmeticException("The wrapped int can not be properly converted"
                            + " to a short");
                }
            }
        }
        //TODO: Finish
        if(classification == Classification.LONG){
            if(type == Classification.BOOLEAN){

            }
            if(type == Classification.BYTE){

            }
            if(type == Classification.CHAR){

            }
            if(type == Classification.DOUBLE){

            }
            if(type == Classification.FLOAT){

            }
            if(type == Classification.INT){

            }
            if(type == Classification.LONG){

            }
            if(type == Classification.SHORT){

            }
        }
        if(classification == Classification.SHORT){
            if(type == Classification.BOOLEAN){

            }
            if(type == Classification.BYTE){

            }
            if(type == Classification.CHAR){

            }
            if(type == Classification.DOUBLE){

            }
            if(type == Classification.FLOAT){

            }
            if(type == Classification.INT){

            }
            if(type == Classification.LONG){

            }
            if(type == Classification.SHORT){

            }
        }
        throw new WrongClassTypeException("getAsType does not support casting the value of type " +
                classification.name() + " to " + type.name());
    }

    /**
     * Gets the class type of this object. This is valid for all classifications,
     * except null types, because no class value can be derived for null.
     * If you know the class type, but the object itself is null, use the
     * {@code getNullType(Class)} function instead of the
     * {@code exObject(Object)} constructor.
     * type, and simply send null.
     * @return the class of the contained value.
     * @throws SparkTabCore.exObject.WrongClassTypeException
     */
    public Class getClassType() throws WrongClassTypeException{
        if(classification == Classification.NULL){
            throw new WrongClassTypeException("Value is null, no Class type");
        } else {
            return type;
        }
    }

    /**
     * @return The classification of this value, one of the {@code Classification} values.
     * @see Classification
     */
    public Classification getClassification(){
        return classification;
    }

    /**
     * This returns the toString representation of the value; regardless
     * of what type it is. The appropriate toString function is called
     * when the wrapped value is a primitive. Void returns an empty string,
     * and a null object returns "null".
     * @return a String
     */
    @Override
    public String toString(){
        if(classification == Classification.NULL){
            return "null";
        } else if(classification == Classification.VOID){
            return "";
        } else if(classification == Classification.OBJECT){
            return objectVal.toString();
        } else if(classification == Classification.STRING){
            return stringVal;
        } else if(classification == Classification.BOOLEAN){
            return Boolean.toString(booleanVal);
        } else if(classification == Classification.BYTE){
            return Byte.toString(byteVal);
        } else if(classification == Classification.CHAR){
            return Character.toString(charVal);
        } else if(classification == Classification.DOUBLE){
            return Double.toString(doubleVal);
        } else if(classification == Classification.FLOAT){
            return Float.toString(floatVal);
        } else if(classification == Classification.INT){
            return Integer.toString(intVal);
        } else if(classification == Classification.LONG){
            return Long.toString(longVal);
        } else if(classification == Classification.SHORT){
            return Short.toString(shortVal);
        } else {
            return "null";
        }
    }

    public String toDetailedString(){
        if(classification == Classification.OBJECT){
            return classification.name() + "(" + objectVal.getClass().getName() + ") " + toString();
        } else if(classification == Classification.VOID) {
            return classification.name();
        } else {
            return classification.name() + " " + toString();
        }
    }

    /**
     * This method returns the wrapped object as if it were an object,
     * regardless of whether or not it is a primitive. The only
     * exception is if it wraps a void type object, in which case
     * it returns null. This allows you to get the object and use it
     * without checking to see what it's type is. In most cases, this
     * is perfectly acceptable, because autoboxing will handle the
     * details of converting it to a primitive if needed.
     * @return
     */
    public Object toObject(){
        if(classification == Classification.NULL){
            return null;
        } else if(classification == Classification.VOID){
            return null;
        } else if(classification == Classification.OBJECT){
            return objectVal;
        } else if(classification == Classification.STRING){
            return stringVal;
        } else if(classification == Classification.BOOLEAN){
            return Boolean.valueOf(booleanVal);
        } else if(classification == Classification.BYTE){
            return Byte.valueOf(byteVal);
        } else if(classification == Classification.CHAR){
            return Character.valueOf(charVal);
        } else if(classification == Classification.DOUBLE){
            return Double.valueOf(doubleVal);
        } else if(classification == Classification.FLOAT){
            return Float.valueOf(floatVal);
        } else if(classification == Classification.INT){
            return Integer.valueOf(intVal);
        } else if(classification == Classification.LONG){
            return Long.valueOf(longVal);
        } else if(classification == Classification.SHORT){
            return Short.valueOf(shortVal);
        } else {
            return null;
        }
    }

    /**
     * This function allows you to set the value of the wrapped value (if it's one
     * of the 8 primitive types) from a string that represents the value. This function
     * uses the same parsing engine that {@code getPrimitive} uses, so if the value
     * fails parsing there, it will fail here too. Setting the value in this way will
     * also notify the change listeners.
     * @param val
     */
    public void setValue(String val){
        if(classification == Classification.NULL){
            throw new WrongClassTypeException("Trying to set a nulltype to a value");
        } else if(classification == Classification.VOID){
            throw new WrongClassTypeException("Trying to set a void type to a value");
        } else if(classification == Classification.OBJECT){
            throw new WrongClassTypeException("Trying to set an object from a String");
        } else if(classification == Classification.STRING){
            stringVal = val;
        } else if(classification == Classification.BOOLEAN){
            booleanVal = exObject.getPrimitive("boolean", val).getBoolean();
        } else if(classification == Classification.BYTE){
            byteVal = exObject.getPrimitive("byte", val).getByte();
        } else if(classification == Classification.CHAR){
            charVal = exObject.getPrimitive("char", val).getChar();
        } else if(classification == Classification.DOUBLE){
            doubleVal = exObject.getPrimitive("double", val).getDouble();
        } else if(classification == Classification.FLOAT){
            floatVal = exObject.getPrimitive("float", val).getFloat();
        } else if(classification == Classification.INT){
            intVal = exObject.getPrimitive("int", val).getInt();
        } else if(classification == Classification.LONG){
            longVal = exObject.getPrimitive("long", val).getLong();
        } else if(classification == Classification.SHORT){
            shortVal = exObject.getPrimitive("short", val).getShort();
        } else {
            return;
        }
        notifyListeners();
    }

    //TODO: Possibly add more setValue functions that take objects or primitives
    //as the argument, and piggyback off the getAsType function

    /**
     * This function activates the change listeners that are associated with
     * this object.
     */
    private void notifyListeners(){
        for(exObjectChangeListener l : listeners){
            l.variableChanged(this);
        }
    }

    /**
     * This function adds a listener to this exObject, that gets notified whenever
     * the wrapped value changes.
     * @param l The listener to register with this object
     * @return Whether or not the listener was added
     * @see exobject.exObjectChangeListener
     */
    public boolean addListener(exObjectChangeListener l){
        if(listeners == null){
            listeners = new ArrayList<exObjectChangeListener>();
        }
        return listeners.add(l);
    }

    /**
     * This function removes the specified listener from this exObject.
     * @param l The listener to remove
     * @return Whether or not the listener was removed
     * @see exobject.exObjectChangeListener
     */
    public boolean removeListener(exObjectChangeListener l){
        return listeners.remove(l);
    }



    @Override
    public int hashCode() {
        int hash = 7;
        hash = 79 * hash + (this.type != null ? this.type.hashCode() : 0);
        hash = 79 * hash + (this.classification != null ? this.classification.hashCode() : 0);
        hash = 79 * hash + (this.objectVal != null ? this.objectVal.hashCode() : 0);
        hash = 79 * hash + (this.stringVal != null ? this.stringVal.hashCode() : 0);
        hash = 79 * hash + this.byteVal;
        hash = 79 * hash + this.shortVal;
        hash = 79 * hash + this.intVal;
        hash = 79 * hash + (int) (this.longVal ^ (this.longVal >>> 32));
        hash = 79 * hash + Float.floatToIntBits(this.floatVal);
        hash = 79 * hash + (int) (Double.doubleToLongBits(this.doubleVal) ^ (Double.doubleToLongBits(this.doubleVal) >>> 32));
        hash = 79 * hash + (this.booleanVal ? 1 : 0);
        hash = 79 * hash + this.charVal;
        hash = 79 * hash + (this.listeners != null ? this.listeners.hashCode() : 0);
        return hash;
    }

    /**
     * This function returns whether or not two primitives are equivalent. The
     * equals function would return false for {@code new exObject(1)} and
     * {@code new exObject(1.0)}, even though these are equivalent. This will
     * still return a value if the wrapped value is an Object, but it will just
     * call .equals on the object.
     * @param obj
     * @return
     */
    public boolean equivalent(Object obj){
        throw new RuntimeException("Not implemented yet");
    }
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final exObject other = (exObject) obj;
        if (this.type != other.type && (this.type == null || !this.type.equals(other.type))) {
            return false;
        }
        if (this.classification != other.classification) {
            return false;
        }
        if (this.objectVal != other.objectVal && (this.objectVal == null || !this.objectVal.equals(other.objectVal))) {
            return false;
        }
        if ((this.stringVal == null) ? (other.stringVal != null) : !this.stringVal.equals(other.stringVal)) {
            return false;
        }
        if (this.byteVal != other.byteVal) {
            return false;
        }
        if (this.shortVal != other.shortVal) {
            return false;
        }
        if (this.intVal != other.intVal) {
            return false;
        }
        if (this.longVal != other.longVal) {
            return false;
        }
        if (Float.floatToIntBits(this.floatVal) != Float.floatToIntBits(other.floatVal)) {
            return false;
        }
        if (Double.doubleToLongBits(this.doubleVal) != Double.doubleToLongBits(other.doubleVal)) {
            return false;
        }
        if (this.booleanVal != other.booleanVal) {
            return false;
        }
        if (this.charVal != other.charVal) {
            return false;
        }
        if (this.listeners != other.listeners && (this.listeners == null || !this.listeners.equals(other.listeners))) {
            return false;
        }
        return true;
    }





    /*/ Void Type **************************************************************/
    /**
     * Since it is not possible to send a void type object, you must use this
     * static method that returns a new {@code exObject}, instantiated as a
     * void type object.
     * @return a new void type object
     */
    public static exObject getVoidType(){
        exObject o = new exObject();
        o.type = void.class;
        o.classification = Classification.VOID;
        return o;
    }

    /**
     * @return Whether or not the wrapped value is a void type object
     */
    public boolean isVoidType(){
        return classification == Classification.VOID;
    }

    /*/ Null Type **************************************************************/
    /**
     * Since {@code new exObject(null)} would return a null value with classification
     * {@code OBJECT}, use this static method to return a new null type {@code exObject}
     * instead. If you know the type of the object, but it is null, (or unset, in the
     * case of primitives), you should use {@code getNullType(Class)} instead.
     * @return a null value object
     * @see getNullType(Class)
     */
    public static exObject getNullType() throws WrongClassTypeException{
        return getNullType(null);
    }

    /**
     * This static function returns a null object with type {@code type}. There
     * actually is no equivalent null value for a primitive, so if {@code type}
     * is a primitive (or void), an exception is thrown.
     * @param type
     * @return
     */
    public static exObject getNullType(Class type) throws WrongClassTypeException{
        exObject o = new exObject();
        o.type = type;
        if(type.equals(byte.class)){
            throw new WrongClassTypeException("Assigning null to byte primitive");
        } else if(type.equals(short.class)){
            throw new WrongClassTypeException("Assigning null to short primitive");
        } else if(type.equals(int.class)){
            throw new WrongClassTypeException("Assigning null to int primitive");
        } else if(type.equals(long.class)){
            throw new WrongClassTypeException("Assigning null to long primitive");
        } else if(type.equals(float.class)){
            throw new WrongClassTypeException("Assigning null to float primitive");
        } else if(type.equals(double.class)){
            throw new WrongClassTypeException("Assigning null to double primitive");
        } else if(type.equals(boolean.class)){
            throw new WrongClassTypeException("Assigning null to boolean primitive");
        } else if(type.equals(char.class)){
            throw new WrongClassTypeException("Assigning null to char primitive");
        } else if(type.equals(void.class)){
            throw new WrongClassTypeException("Assigning null to void");
        } else if(type == null){
            o.classification = Classification.NULL;
        } else if(type.equals(String.class)){
            o.classification = Classification.STRING;
        } else{
            o.classification = Classification.OBJECT;
        }
        return o;
    }

    /**
     * @return Whether or not the wrapped value is a null type object
     */
    public boolean isNullType(){
        return classification == Classification.NULL;
    }
    /*/ Object Type ***********************************************************/

    /**
     * Creates a new {@code exObject} based on object {@code o}. If {@code o} is
     * one of the primitive wrappers, it is unboxed, and the proper primitive type
     * is created instead of a generic Object type.
     * @param o
     */
    public exObject(Object o){
        type = o.getClass();
        classification = Classification.OBJECT;
        //If the object is a boxed value, unbox it.
        if(o.getClass() == Long.class){
            longVal = ((Long)o).longValue();
        } else if(o.getClass() == Float.class){
            floatVal = ((Float)o).floatValue();
        } else if(o.getClass() == Double.class){
            doubleVal = ((Double)o).doubleValue();
        } else if(o.getClass() == Boolean.class){
            booleanVal = ((Boolean)o).booleanValue();
        } else if(o.getClass() == Character.class){
            charVal = ((Character)o).charValue();
        } else if(o.getClass() == Integer.class){
            intVal = ((Integer)o).intValue();
        } else if(o.getClass() == Byte.class){
            byteVal = ((Byte)o).byteValue();
        } else if(o.getClass() == Short.class){
            shortVal = ((Short)o).shortValue();
        } else {
            objectVal = o;
        }
    }

    /**
     * Returns whether or not the wrapped object is an Object
     * @return
     */
    public boolean isObjectType(){
        return classification == Classification.OBJECT;
    }

    /**
     * Returns the wrapped object. If the wrapped value is not an object, and you
     * want the object value of it anyways, use {@code toObject} instead, which
     * will never throw an exception. If the wrapped value is not an object,
     * and you call this function, it throws a {@code WrongClassTypeException}.
     * @return
     * @throws WrongClassTypeException
     */
    public Object getObject() throws WrongClassTypeException{
        if(classification != Classification.OBJECT){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for OBJECT");
        }
        return objectVal;
    }

    /**
     * Sets the wrapped Object to o
     * @param o
     * @throws WrongClassTypeException
     */
    public void setObject(Object o) throws WrongClassTypeException{
        if(classification != Classification.OBJECT){
            throw new WrongClassTypeException("Trying to set OBJECT to  " + classification.name() + " type value");
        }
        objectVal = o;
        this.notifyListeners();
    }

    /*/ String Type ***********************************************************/

    /**
     * Constructs a new {@code exObject} of type {@code String}. If null is
     * sent, then this is the constructor that gets called. In this case, a
     * nulltype object is created, with a null classification. If you would
     * like a nulltype with an actual type, you must use the
     * {@code getNullType(Class)} builder instead.
     * @param s
     */
    public exObject(String s){
        //this is the constructor that gets called if null is passed, so
        //we will instead return a null object if s is null. We can't determine
        //the type of the object though, so it will be a real nulltype, instead
        //of a string nulltype.
        if(s == null){
            type = null;
            classification = Classification.NULL;
        } else{
            type = String.class;
            stringVal = s;
            classification = Classification.STRING;
        }
    }

    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code String}
     */
    public boolean isStringType(){
        return classification == Classification.STRING;
    }

    /**
     * Returns the string object that is wrapped by this object. If you want the
     * toString value, instead of the string object, use {@code  toString} instead,
     * as this will throw a {@code  WrongClassTypeException} if the wrapped value
     * is not a string.
     * @return
     * @throws WrongClassTypeException
     */
    public String getString() throws WrongClassTypeException{
        if(classification != Classification.STRING){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for STRING");
        }
        return stringVal;
    }
    public void setString(String s) throws WrongClassTypeException{
        if(classification != Classification.STRING){
            throw new WrongClassTypeException("Trying to set STRING to " + classification.name() + " type value");
        }
        stringVal = s;
        this.notifyListeners();
    }

    /*/ Long Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code long}
     */
    public exObject(long l){
        type = long.class;
        longVal = l;
        classification = Classification.LONG;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code long}
     */
    public boolean isLongType(){
        return classification == Classification.LONG;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type long
     */
    public long getLong() throws WrongClassTypeException{
        if(classification != Classification.LONG){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for LONG");
        }
        return longVal;
    }
    /**
     * Sets the wrapped value to l
     * @param l the long to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setLong(long l) throws WrongClassTypeException{
        if(classification != Classification.LONG){
            throw new WrongClassTypeException("Trying to set LONG to " + classification.name() + " type value");
        }
        longVal = l;
        this.notifyListeners();
    }

    /*/ Float Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code float}
     */
    public exObject(float f){
        type = float.class;
        floatVal = f;
        classification = Classification.FLOAT;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code float}
     */
    public boolean isFloatType(){
        return classification == Classification.FLOAT;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type float
     */
    public float getFloat() throws WrongClassTypeException{
        if(classification != Classification.FLOAT){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for FLOAT");
        }
        return floatVal;
    }
    /**
     * Sets the wrapped value to f
     * @param f the float to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setFloat(float f) throws WrongClassTypeException{
        if(classification != Classification.FLOAT){
            throw new WrongClassTypeException("Trying to set FLOAT to " + classification.name() + " type value");
        }
        floatVal = f;
        this.notifyListeners();
    }

    /*/ Double Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code double}
     */
    public exObject(double d){
        type = double.class;
        doubleVal = d;
        classification = Classification.DOUBLE;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code double}
     */
    public boolean isDoubleType(){
        return classification == Classification.DOUBLE;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type double
     */
    public double getDouble() throws WrongClassTypeException{
        if(classification != Classification.DOUBLE){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for DOUBLE");
        }
        return doubleVal;
    }
    /**
     * Sets the wrapped value to d
     * @param d the double to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setDouble(double d) throws WrongClassTypeException{
        if(classification != Classification.DOUBLE){
            throw new WrongClassTypeException("Trying to set DOUBLE to " + classification.name() + " type value");
        }
        doubleVal = d;
        this.notifyListeners();
    }

    /*/ Boolean Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code boolean}
     */
    public exObject(boolean b){
        type = boolean.class;
        booleanVal = b;
        classification = Classification.BOOLEAN;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code boolean}
     */
    public boolean isBooleanType(){
        return classification == Classification.BOOLEAN;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type boolean
     */
    public boolean getBoolean() throws WrongClassTypeException{
        if(classification != Classification.BOOLEAN){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for BOOLEAN");
        }
        return booleanVal;
    }
    /**
     * Sets the wrapped value to b
     * @param b the boolean to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setBoolean(boolean b) throws WrongClassTypeException{
        if(classification != Classification.BOOLEAN){
            throw new WrongClassTypeException("Trying to set BOOLEAN to " + classification.name() + " type value");
        }
        booleanVal = b;
        this.notifyListeners();
    }

    /*/ Char Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code char}
     */
    public exObject(char c){
        type = char.class;
        charVal = c;
        classification = Classification.CHAR;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code char}
     */
    public boolean isCharType(){
        return classification == Classification.CHAR;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type char
     */
    public char getChar() throws WrongClassTypeException{
        if(classification != Classification.CHAR){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for CHAR");
        }
        return charVal;
    }
    /**
     * Sets the wrapped value to c
     * @param c the char to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setChar(char c) throws WrongClassTypeException{
        if(classification != Classification.CHAR){
            throw new WrongClassTypeException("Trying to set CHAR to " + classification.name() + " type value");
        }
        charVal = c;
        this.notifyListeners();
    }

    /*/ Int Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code int}
     */
    public exObject(int i){
        type = int.class;
        intVal = i;
        classification = Classification.INT;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code int}
     */
    public boolean isIntType(){
        return classification == Classification.INT;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type int
     */
    public int getInt() throws WrongClassTypeException{
        if(classification != Classification.INT){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for INT");
        }
        return intVal;
    }
    /**
     * Sets the wrapped value to i
     * @param i the int to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setInt(int i) throws WrongClassTypeException{
        if(classification != Classification.INT){
            throw new WrongClassTypeException("Trying to set INT to " + classification.name() + " type value");
        }
        intVal = i;
        this.notifyListeners();
    }

    /*/ Byte Type ***********************************************************/
    /**
     * Since bytes are ints, it is not possible to create a byte object with the same
     * signature as the int constructor. For this reason, bytes have a special constructor
     * that allows them to be created. Alternately, you can use the static byte creator.
     * @param b The byte to store
     * @param c A Class object. Can be any class, but typically, use {@code byte.class} to
     * make it more obvious.
     */
    public exObject(byte b, Class c){
        type = byte.class;
        byteVal = b;
        classification = Classification.BYTE;
    }

    /**
     * The static version of the byte constructor
     * @param b the byte to wrap
     * @return a new exObject
     */
    public static exObject getByteType(byte b){
        exObject o = new exObject();
        o.type = byte.class;
        o.byteVal = b;
        o.classification = Classification.BYTE;
        return o;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code byte}
     */
    public boolean isByteType(){
        return classification == Classification.BYTE;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type byte
     */
    public byte getByte() throws WrongClassTypeException{
        if(classification != Classification.BYTE){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for BYTE");
        }
        return byteVal;
    }
    /**
     * Sets the wrapped value to b
     * @param b the byte to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setByte(byte b) throws WrongClassTypeException{
        if(classification != Classification.BYTE){
            throw new WrongClassTypeException("Trying to set BYTE to " + classification.name() + " type value");
        }
        byteVal = b;
        this.notifyListeners();
    }

    /*/ Short Type ***********************************************************/
    /**
     * Constructs a new {@code exObject} of type {@code short}
     */
    public exObject(short s){
        type = short.class;
        shortVal = s;
        classification = Classification.SHORT;
    }
    /**
     * @return whether or not the wrapped {@code exObject} is of type {@code short}
     */
    public boolean isShortType(){
        return classification == Classification.SHORT;
    }
    /**
     * Gets the wrapped value.
     * @throws WrongClassTypeException if the wrapped value is not of type short
     */
    public short getShort() throws WrongClassTypeException{
        if(classification != Classification.SHORT){
            throw new WrongClassTypeException("Value type is " + classification.name() + ", asked for SHORT");
        }
        return shortVal;
    }
    /**
     * Sets the wrapped value to s
     * @param s the short to set the wrapped value to.
     * @throws WrongClassTypeException
     */
    public void setShort(short s) throws WrongClassTypeException{
        if(classification != Classification.SHORT){
            throw new WrongClassTypeException("Trying to set SHORT to " + classification.name() + " type value");
        }
        shortVal = s;
        this.notifyListeners();
    }
}
